CQF Exam 2¶

William Harris¶

Introduction¶

Asian and lookback options are a type of exotic option which are classified as being strongly path dependent. This is due to the fact that their payoffs depend on the entire underlying's asset price path. As a results, Monte Carlo simulations are a good numerical solution choice for estimating their price. This report will use Monte Carlo simulations to calculate Asian and lookback option prices and provide analysis and insights into how these option prices change from variation in their initial conditions and numerical solution implementation.

Asian Options¶

Asian options give the holder a payoff which depends on the average price of the underlying over the life of the option. This averaging is subject to two characteristics; type which is either arithmetic or geometric and frequency which is either discrete or continuous.

\begin{array}{c|cc} \textbf{A} & Arithmetic & Geometric\\ \hline Discrete & \frac{1}{n}\sum_{i=1}^{n} S(t_i) & e^{\frac{1}{n} \sum_{i=1}^{n} log(S(t_i))} \\ Continuous & \frac{1}{T} \int_{0}^{T} S(t) dt & e^{\frac{1}{T} \int_{0}^{T} log(S(t)) dt} \\ \end{array}

Here $i$ denotes a particular sampling date, $n$ is the total number of sampling dates and $T$ is the expiry date. Discrete frequencies subject the averaging to particular sampling dates, while continuous frequencies calculate an average from all observed prices. Additionally, there are two types of payoff variations, fixed strike and floating strike.

\begin{array}{c|cc} \textbf{Payoff} & Call & Put\\ \hline Fixed Strike & \max(A-K,0) & \max(K-A,0) \\ Floating Strike & \max(S_T-A,0) & \max(A - S_T,0) \\ \end{array}

Lookback Options¶

Lookback options give the holder a payoff which depends on the maximum or minimum price of the underlying over the life of the option. Similarly to Asian options, lookback options have two types of frequencies, discrete or continuous. The frequency determines when the maximum or minimum prices are observed.

\begin{array}{c|cc} \textbf{Frequency} & S_{min} & S_{max}\\ \hline Discrete & \min_{\forall i \in D}(S_i) & \max_{\forall i \in D}(S_i) \\ Continuous & \min_{0 \leq t \leq T}(S_t) & \max_{0 \leq t \leq T}(S_t) \\ \end{array}

Here $D$ is the set of all sampling dates and $T$ is the expiry date. Lookback options also have two payoff variations, fixed strike and floating strike.

\begin{array}{c|cc} \textbf{Payoff} & Call & Put\\ \hline Fixed Strike & \max(S_{max}-K,0) & \max(K-S_{min},0) \\ Floating Strike & S_T-S_{min} & S_{max}-S_T \\ \end{array}

Define the Euler-Maruyama Method¶

The Euler-Maruyama method can be applied to stochastic differential equations (SDE) to find an approximation form. This method becomes helpful when deriving the risk-neutral random walk equation.

Consider the following SDE:

\begin{equation*} dX_t = a(X_t, t)dt + b(X_t, t)dW_t \end{equation*}

Integrate both sides by a discrete time interval.

\begin{equation*} \int_{t_{n}}^{t_n + 1} dX_s = \int_{t_{n}}^{t_n + 1} a(X_s, s)ds + \int_{t_{n}}^{t_n + 1} b(X_s, s)dW_s \tag{1} \end{equation*}

Then, the left hand integration rule is applied to each term to find its approximation.

\begin{equation*} \int_{t_{n}}^{t_n + 1} a(X_s, s)ds \approx a(X_n, n) \int_{t_{n}}^{t_n + 1} ds = a(X_n, n) \delta t \end{equation*}\begin{equation*} \int_{t_{n}}^{t_n + 1} b(X_s, s)dW_s \approx b(X_n, n) \int_{t_{n}}^{t_n + 1} dW_s = b(X_n, n) \Delta W_n \end{equation*}

Here $n$ is the timestep and $\Delta W_n$ is normally distributed with a mean of zero and a standard deviation of $\sqrt{\delta t}$. Therefore, $\Delta W_n$ can be expressed as $\phi \sqrt{\delta t}$ with $\phi \sim \mathcal{N}(0, 1)$.

Substituting these into equation $(1)$ produces the Euler-Maruyama approximation.

\begin{equation*} X_{n+1} = X_n + a(X_n, t_n) \delta t + b(X_n, t_n) \phi \sqrt{\delta t} \end{equation*}

It is important to remember that this Euler-Maruyama's has an accuracy of $O(\delta t^{1/2}$).

Define the Risk-Neutral Random Walk¶

An options underlying can be modelled with the following Geometric Brownian Motion SDE: \begin{equation*} dS =\mu S dt + \sigma S dW \end{equation*}

Here the constants are $\mu$ is the percentage drift and $\sigma$ is the percentage volatility.

The Euler-Maruyama method is applied to the SDE by defining $a(S_t, t) = \mu S_t$ and $b(S_t, t) = \sigma S_t$.

\begin{equation*} S_{t+\delta t} \approx S_t + \mu S_t \delta t + \sigma S_t \phi \sqrt{\delta t} = S_t (1 + \mu\delta t + \sigma \phi \sqrt{\delta t}) \end{equation*}

Under the risk-neutral measure $\mathbb{Q}$ all associated risk has been removed. Therefore, the percentage drift $\mu$ is only subject to the risk-free rate $r$. This defines the risk-neutral random walk equation.

\begin{equation*} S_{t+\delta t} = S_t (1 + r\delta t + \sigma \phi \sqrt{\delta t}) \tag{2} \end{equation*}

Monte-Carlo Simulations for estimating Option Prices¶

The price of an option is defined by the present value of the option's payoff where the underlying follows a risk-neutral random walk.

\begin{equation*} V(S,t) = e^{-r(T-t)} \mathbb{E}^\mathbb{Q}[Payoff(S_T)] \end{equation*}

Considering that the risk-neutral random walk simulations are randomly generated independent samples, the average option payoff across all simulations can estimate the option's expected payoff.

\begin{equation*} \mathbb{E}^\mathbb{Q}[Payoff(S_T)] \approx \frac{1}{n} \sum_{i=1}^n Payoff(S_T^i) \end{equation*}

Here $i$ denotes a particular simulated path and $n$ is the total number of simulations. By the law of large numbers, as $n \rightarrow \infty$ the estimated payoff converges to the actual payoff.

Thus, substituting back into the options price equation.

\begin{equation*} V(S,t) \approx e^{-r(T-t)} \frac{1}{n} \sum_{i=1}^n Payoff(S_T^i) \tag{3} \end{equation*}

Implementation¶

Firstly, the relevant modules which are used throughout the Jupyer Notebook are imported.

In [ ]:
from enum import Enum 
from itertools import product
from typing import Dict, Type, Optional
from functools import lru_cache
from abc import ABC, abstractmethod
from dataclasses import dataclass, asdict

import plotly
import numpy as np
import pandas as pd
from tqdm import tqdm
import plotly.io as pio
import plotly.graph_objects as go
from scipy.interpolate import griddata
from plotly.subplots import make_subplots
import plotly.express as px

pio.renderers.default='notebook'
plotly.offline.init_notebook_mode()

Next, the risk-neutral random walk equation $(2)$ is implemented into the function $\tt{simulate\_path}$. The function can generate a set of simulations by specifying the number of desired simulations in the variable $\tt{n\_sims}$. The python inbuilt functools function $\tt{lru\_cache}$ is added to $\tt{simulate\_path}$ as a decorator to cache simulated paths to memory. This becomes useful when simulated paths with the same input variables are called more than once. Additionally, the variance reduction technique antithetic variances has been added to the creation of standard normal random numbers. This will increase the accuracy of the Monte Carlo simulations by introducing negative dependencies on the draw of random numbers and create a true set of standard normal numbers which have a mean of zero and a standard deviation of one.

In [ ]:
@lru_cache(maxsize=None)
def simulate_path(s0: float, r: float, vol: float, time_to_expiry: float, timesteps: int, n_sims: int, apply_var_reduction=True, seed_num=0) -> np.array:  
    np.random.seed(seed_num)  
    dt = time_to_expiry/timesteps
    S = np.zeros((timesteps, n_sims))
    S[0] = s0

    for i in range(0, timesteps-1):
        if apply_var_reduction:
            w = np.random.standard_normal(n_sims//2)
            w = np.concatenate((w,-w), axis=0)
        else:
            w = np.random.standard_normal(n_sims)
        S[i+1] = S[i] * (1 + r * dt + vol * np.sqrt(dt) * w)
        
    return S

As an example, 10 stock price simulations are generated with the input conditions of today's stock price $S_0 = 100$, constant risk-free interest rate $r = 0.05$, volatility $vol = 0.2$, $time\_to\_expiry = 1$ and $timesteps = 252$.

In [ ]:
simulations = simulate_path(100, 0.05, 0.20, 1, 252, 10, apply_var_reduction=False)
fig = go.Figure([go.Scatter(x=list(range(0, 252)), y=sim, name=f'i={i+1}') for i, sim in enumerate(simulations.T)])
fig.update_layout(title='Monte Carlo Simulations', 
                  xaxis_title='Timesteps', 
                  yaxis_title='Asset Price', 
                  width=700, 
                  showlegend=False)
fig.show()

A few classes below are defined which help in investigating the affects on Asian and lookback option prices from changes in their input data. The Abstract Class $\tt{BaseOption}$ is the blueprint for the exotic options. This class defines the init constructor which most importantly calls $\tt{simulate\_path}$ and generates the option's simulated paths. In addition, Two Enums $\tt{AveragingType}$ and $\tt{StrikeType}$ hold information about the possible choices of averaging types and strike types and a dataclass $\tt{StrikeInfo}$ will hold the strike type and strike amount data.

In [ ]:
class BaseOption(ABC):
    "Abstract Class for Options"
    def __init__(self, s0: float, r: float, vol: float, time_to_expiry: float, timesteps: int, n_sims: int):
        self._s0 = s0
        self._r = r
        self._vol = vol
        self._time_to_expiry = time_to_expiry
        self._timesteps = timesteps
        self._n_sims = n_sims
        self._sims = simulate_path(s0, r, vol, time_to_expiry, timesteps, n_sims)

    @abstractmethod
    def price(self):
        pass


class AveragingType(Enum):
    Arithmetic = 1
    Geometric = 2


class StrikeType(Enum):
    Fixed = 1
    Floating = 2

 
@dataclass(frozen=True, eq=True, unsafe_hash=True)
class StrikeInfo:
    strike_type: StrikeType
    strike: Optional[float] = None

    def __post_init__(self):
        if self.strike_type == StrikeType.Fixed and self.strike is None:
            raise Exception("Fixed Strikes need a strike")
        if self.strike_type == StrikeType.Floating and self.strike is not None:
            raise Exception("Floating Strikes do not need a strike")

The $\tt{AsianOption}$ and $\tt{LookbackOption}$ classes hold the functionality which calculates the present value of the option's payoff defined in equation $(3)$. This report has only implemented continuous frequency types, further analysis could be done to investigate the differences in option prices between continuous and discrete frequency types.

In [ ]:
class AsianOption(BaseOption):
    "Class representing an Asian Option"

    def __init__(self, averaging_type: AveragingType, strike_info: StrikeInfo, *args, **kwargs):
        if not isinstance(strike_info, StrikeInfo):
            raise ValueError(f"Unknown StrikeInfo {strike_info}")
        
        if not isinstance(averaging_type, AveragingType):
            raise ValueError(f"Unknown Averaging Type {averaging_type}")

        super().__init__(*args, **kwargs)
        self._averaging_type = averaging_type
        self._strike_info = strike_info

    def __fixed_strike(self, _sim_averages: np.array) -> Dict[str, float]:
        call = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(_sim_averages - self._strike_info.strike, 0))
        put = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(self._strike_info.strike - _sim_averages, 0))
        return {'call': call, 'put': put}
    
    def __floating_strike(self, _sim_averages: np.array) -> Dict[str, float]:
        call = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(self._sims[-1] - _sim_averages, 0))
        put = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(_sim_averages  - self._sims[-1], 0))
        return {'call': call, 'put': put}

    def __fixed_strike_arithmetic(self) -> Dict[str, float]:
        _sim_averages = self._sims.mean(axis=0) 
        return self.__fixed_strike(_sim_averages)
    
    def __floating_strike_arithmetic(self) -> Dict[str, float]:
        _sim_averages = self._sims.mean(axis=0) 
        return self.__floating_strike(_sim_averages)
    
    def __fixed_strike_geometric(self) -> Dict[str, float]:
        _sim_averages = np.exp(np.mean(np.log(self._sims), axis=0))
        return self.__fixed_strike(_sim_averages)
    
    def __floating_strike_geometric(self) -> Dict[str, float]:
        _sim_averages = np.exp(np.mean(np.log(self._sims), axis=0))
        return self.__floating_strike(_sim_averages)
    
    def price(self):
        mapping = {(AveragingType.Arithmetic, StrikeType.Fixed): self.__fixed_strike_arithmetic,
                   (AveragingType.Arithmetic, StrikeType.Floating): self.__floating_strike_arithmetic,
                   (AveragingType.Geometric, StrikeType.Fixed): self.__fixed_strike_geometric,
                   (AveragingType.Geometric, StrikeType.Floating): self.__floating_strike_geometric}
        
        pricer = mapping.get((self._averaging_type, self._strike_info.strike_type))
        return pricer()


class LookbackOption(BaseOption):
    "Class representing an Lookback Option"
    
    def __init__(self, strike_info: StrikeInfo, *args, **kwargs):
        if not isinstance(strike_info, StrikeInfo):
            raise ValueError(f"Unknown Strike Info {strike_info}")

        super().__init__(*args, **kwargs)
        self._strike_info = strike_info
        self._sim_maxs = np.amax(self._sims, axis=0) 
        self._sim_mins = np.amin(self._sims, axis=0)   

    def __fixed_strike(self) -> Dict[str, float]:
        call = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(self._sim_maxs - self._strike_info.strike, 0))
        put = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(self._strike_info.strike - self._sim_mins, 0))
        return {'call': call, 'put': put}

    def __floating_strike(self) -> Dict[str, float]:
        call = np.exp(-self._r*self._time_to_expiry) * np.mean(self._sims[-1] - self._sim_mins)
        put = np.exp(-self._r*self._time_to_expiry) * np.mean(self._sim_maxs - self._sims[-1])
        return {'call': call, 'put': put}
    
    def price(self):
        mapping = {StrikeType.Fixed: self.__fixed_strike, StrikeType.Floating: self.__floating_strike}
        pricer = mapping.get(self._strike_info.strike_type)
        return pricer()
In [ ]:
price = AsianOption(averaging_type=AveragingType.Arithmetic, strike_info=StrikeInfo(StrikeType.Fixed, 100), s0=100, r=0.05, vol=0.20, time_to_expiry=1, timesteps=252, n_sims=10000).price()
f"Asian Option: Call Price = {price['call']:.2f} and Put Price {price['put']:.2f}"
Out[ ]:
'Asian Option: Call Price = 5.72 and Put Price 3.33'
In [ ]:
price = LookbackOption(strike_info=StrikeInfo(StrikeType.Fixed, 100), s0=100, r=0.05, vol=0.20, time_to_expiry=1, timesteps=252, n_sims=10000).price()
f"Lookback Option: Call Price = {price['call']:.2f} and Put Price {price['put']:.2f}"
Out[ ]:
'Lookback Option: Call Price = 18.11 and Put Price 11.60'

The $\tt{Compare}$ class below creates an easy way to generate and compare a combination of BaseOption type classes. This is done by defining a list of possible values for each BaseOption variable. The init constructor then 1. produces a list of all possible option combinations, 2. initialises each combination into the specified BaseOption and 3. prices each of them.

In [ ]:
class Compare:
    def __init__(self, option: Type[BaseOption], **kwargs):
        self.combinations = self.__get_combinations(**kwargs)
        self.options = [option(**combination) for combination in tqdm(self.combinations)]
        unraveled_combinations = [self.unravel_dataclass(x) for x in self.combinations]
        self.df = pd.DataFrame(list(x | y for x, y in zip(unraveled_combinations, [option.price() for option in tqdm(self.options)])))        

    @staticmethod
    def __get_combinations(**kwargs):
        return [{list(kwargs)[i]: c for i, c in enumerate(prod)} for prod in product(*kwargs.values())]

    @staticmethod
    def unravel_dataclass(x):
        strike_info = x.pop('strike_info')
        return x | asdict(strike_info)

    @staticmethod
    def make_pretty(styler):
        styler.set_table_styles([{'selector': 'caption', 'props': 'caption-side: Top; font-size: 1.25em'},
                                {'selector': 'th:not(.index_name)', 'props': 'text-align: center'}])
        styler.background_gradient(axis=0, cmap='YlGnBu')
        return styler

    def table(self, table_name, row, cols, filter):
        subset_df = self.df.copy()

        for filter in filters:
            subset_df = subset_df.query(filter)
        
        df_pivot = subset_df.pivot(index=row, columns=cols, values=['call', 'put'])
        return df_pivot.rename(columns={'call': 'Call Option', 'put': 'Put Option'}).style.pipe(self.make_pretty).set_caption(table_name)

    def surface_plot(self, plot_name, filters, x_name, y_name):
        subset_df= self.df.copy()

        for filter in filters:
            subset_df = subset_df.query(filter)
            assert len(subset_df) != 0, f"DF is size zero {filter}"

        for col in subset_df:
            if col not in (x_name, y_name, "call", "put"):
                assert len(subset_df[col].unique()) == 1, f"{col} does not have one unique value: {subset_df[col].unique()}"

        x = np.array(subset_df[x_name])
        y = np.array(subset_df[y_name])
        z_call = np.array(subset_df["call"])
        z_put = np.array(subset_df["put"])

        xi = np.linspace(x.min(), x.max(), 100)
        yi = np.linspace(y.min(), y.max(), 100)

        X,Y = np.meshgrid(xi,yi)
        scene = lambda z_name: dict(xaxis_title=x_name.replace("_", " ").capitalize(),
                                    yaxis_title=y_name.replace("_", " ").capitalize(),
                                    zaxis_title=z_name,
                                    xaxis = dict(
                                        backgroundcolor="rgb(200, 200, 230)",
                                        gridcolor="white",
                                        showbackground=True,
                                        zerolinecolor="white"),
                                    yaxis = dict(
                                        backgroundcolor="rgb(230, 200, 230)",
                                        gridcolor="white",
                                        showbackground=True,
                                        zerolinecolor="white"),
                                    zaxis = dict(
                                        backgroundcolor="rgb(230, 230, 200)",
                                        gridcolor="white",
                                        showbackground=True,
                                        zerolinecolor="white"),
                                    camera=dict(center=dict(x=0, y=0, z=0),
                                    eye=dict(x=1.6, y=1.7, z=1.7)))

        Z_call = griddata((x,y),z_call,(X,Y), method='cubic')
        Z_put = griddata((x,y),z_put,(X,Y), method='cubic')

        fig = make_subplots(rows=1, cols=2, horizontal_spacing=0, subplot_titles=['Call Option', 'Put Option'], specs=[[{'type': 'scene', 'is_3d': True}, {'type': 'scene', 'is_3d': True}]])
                    
        fig.add_trace(go.Surface(x=xi,y=yi,z=Z_call, colorbar_x=-0.07, colorscale=px.colors.sequential.Aggrnyl), row=1, col=1)
        fig.add_trace(go.Surface(x=xi,y=yi,z=Z_put, colorscale=px.colors.sequential.Agsunset), row=1, col=2)
        fig.update_annotations(y=0.9, selector={'text':'Call Option'})
        fig.update_annotations(y=0.9, selector={'text':'Put Option'})

        fig.update_layout(title=plot_name, scene = scene("Call"), scene2= scene("Put"),
                            width=800,
                            height=400,
                            margin=dict(
                            r=0, l=0,
                            b=0, t=70)
                        )        
        fig.show()

Results and Observations¶

Two compare variables were generated, one for each option type with the varying option data defined below. The number of simulations and number of timesteps to use for each simulation was carefully considered. These variables are important because they affect the accuracy of the option prices and the performance of the Notebook. For this analysis, 252 timesteps (representing 252 business days in a year) and 10,000 simulations were chosen to find a middle ground between both trade-offs.

  1. Initial Stock Price: [100]
  2. Risk-Free Rate: [0.0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5]
  3. Volatility: [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
  4. Time to Expiry: [1.0, 2.0, 3.0, 4.0, 5.0]
  5. Number of Timesteps: [252]
  6. Number of Monte Carlo Simulations: [10000]
  7. Strike Info: [Fixed[0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200], Floating]
  8. Averaging Types: [Geometic, Arithmetic] (Only for Asian Options)
In [ ]:
CompareAsianOptions = Compare(AsianOption,
    s0=[100],
    r=list(x/100 for x in range(0,55,5)),
    vol=list(x/100 for x in range(20,120,20)),
    time_to_expiry=list(x/100 for x in range(100,600,100)),
    timesteps=[252],
    n_sims=[10000],
    strike_info=[StrikeInfo(StrikeType.Fixed, s) for s in range(20,220, 20)] + [StrikeInfo(StrikeType.Floating)],
    averaging_type=[AveragingType.Geometric, AveragingType.Arithmetic])

CompareLookbackOptions = Compare(LookbackOption,
    s0=[100],
    r=list(x/100 for x in range(0,55,5)),
    vol=list(x/100 for x in range(20,120,20)),
    time_to_expiry=list(x/100 for x in range(100,600,100)),
    timesteps=[252],
    n_sims=[10000],
    strike_info=[StrikeInfo(StrikeType.Fixed, s) for s in range(20,220, 20)] + [StrikeInfo(StrikeType.Floating)])

display(CompareAsianOptions.df.head(5))
display(CompareLookbackOptions.df.head(5))
100%|██████████| 6050/6050 [00:10<00:00, 586.89it/s]
100%|██████████| 6050/6050 [01:07<00:00, 89.01it/s] 
100%|██████████| 3025/3025 [00:25<00:00, 119.38it/s]
100%|██████████| 3025/3025 [00:00<00:00, 11402.64it/s]
s0 r vol time_to_expiry timesteps n_sims averaging_type strike_type strike call put
0 100 0.0 0.2 1.0 252 10000 AveragingType.Geometric StrikeType.Fixed 20.0 79.658405 0.000000
1 100 0.0 0.2 1.0 252 10000 AveragingType.Arithmetic StrikeType.Fixed 20.0 79.982099 0.000000
2 100 0.0 0.2 1.0 252 10000 AveragingType.Geometric StrikeType.Fixed 40.0 59.658405 0.000000
3 100 0.0 0.2 1.0 252 10000 AveragingType.Arithmetic StrikeType.Fixed 40.0 59.982099 0.000000
4 100 0.0 0.2 1.0 252 10000 AveragingType.Geometric StrikeType.Fixed 60.0 39.658488 0.000083
s0 r vol time_to_expiry timesteps n_sims strike_type strike call put
0 100 0.0 0.2 1.0 252 10000 StrikeType.Fixed 20.0 95.924446 0.000000
1 100 0.0 0.2 1.0 252 10000 StrikeType.Fixed 40.0 75.924446 0.000000
2 100 0.0 0.2 1.0 252 10000 StrikeType.Fixed 60.0 55.924446 0.043856
3 100 0.0 0.2 1.0 252 10000 StrikeType.Fixed 80.0 35.924446 2.064148
4 100 0.0 0.2 1.0 252 10000 StrikeType.Fixed 100.0 15.924446 14.214183

The variables $\tt{CompareAsianOption}$ and $\tt{CompareLookbackOption}$ hold all combinations of Asian and lookback option prices. The changes in option prices can be analysed by filtering input data and creating surface plots or pivoted dataframes for the certain values that are being investigated. The base case filters were chosen to be the same as the initial pricing examples with $S_0 = 100$, risk-free interest rate $r = 0.05$, volatility $vol = 0.2$, and $time\_to\_expiry = 1$.

The following section provides analysis on the changes in risk-free rate, volatility and time to expiry against the strike and option prices. In addition, the differences between arithmetic and geometric averaging type and the differences between fixed and floating strikes is explored. To compare the changes across different options moneyness, the strikes are used for the y-axis on the surface plots and across table columns. For reference, the initial stock price has been set to $100$ for all prices, therefore for call options, $strikes \approx 100$ are at the money options (ATM), $strikes \gtrsim 100$ are out of the money (OTM) and $strikes \lesssim 100$ are in the money (ITM). While put options are the inverse, $strikes \lesssim 100$ are OTM and $strikes \gtrsim 100$ are ITM.

1. Risk-Free Rate¶

The risk-free rate impacts two parts of the price estimation

  1. the assets drift in the risk-neutral random walk equation \begin{equation*} S_{t+\delta t} = S_t (1 + \color{red}{\textbf{r}} \delta t + \sigma \phi \sqrt{\delta t}) \end{equation*}
  2. the discounting factor in the average payoffs of Monte Carlo simulations. \begin{equation*} V(S,t) \approx e^{- \color{red}{\textbf{r}} (T-t)} \frac{1}{n} \sum_{i=1}^n Payoff(S_T^i) \end{equation*} As seen from surface plots and the tables below, for most call options there is a positive relationship with the risk-free rate, while put options have a negative relationship. This is driven by the assets drift, which produces higher payoffs for call options while lower for put options. In these situations the assets drift impacts the prices more than the discounting factor. However for very far ITM call options there is an inverse relationship with risk-free rates, this is shown in the strike equals 20 column. This is due to the fact that very ITM options poses high amounts of intrinsic value. Therefore, the discounting factor starts to become more dominant than the positive impact on the options payoff.
In [ ]:
filters =  ["s0 == 100", "vol == 0.2", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic"]
CompareAsianOptions.surface_plot("Asian Option - Risk-free Rate - Base Case", filters, "r", "strike")
filters =  ["s0 == 100", "vol == 0.2", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Option - Risk-free Rate - Base Case", 'r', ['strike'], filters))
filters =  ["s0 == 100", "vol == 0.2", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed"]
CompareLookbackOptions.surface_plot("Lookback Option - Risk-free Rate - Base Case", filters, "r", "strike")
filters =  ["s0 == 100", "vol == 0.2", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareLookbackOptions.table("Lookback Option - Risk-free Rate - Base Case", 'r', ['strike'], filters))
Asian Option - Risk-free Rate - Base Case
  Call Option Put Option
strike 20.000000 80.000000 100.000000 120.000000 180.000000 20.000000 80.000000 100.000000 120.000000 180.000000
r                    
0.000000 79.982099 20.060640 4.559393 0.301332 0.000000 0.000000 0.078541 4.577294 20.319233 80.017901
0.050000 78.489019 21.456574 5.715737 0.488061 0.000000 0.000000 0.041320 3.325072 17.121984 73.707689
0.100000 77.028645 22.759914 6.989720 0.758219 0.000047 0.000000 0.021514 2.348069 14.213316 67.745389
0.150000 75.600353 23.968631 8.346293 1.130816 0.000493 0.000000 0.010756 1.602577 11.601260 62.113416
0.200000 74.203523 25.085389 9.762459 1.617613 0.000911 0.000000 0.005711 1.057397 9.287165 56.794309
0.250000 72.837535 26.112463 11.206506 2.238982 0.001302 0.000000 0.002975 0.673034 7.281526 51.771893
0.300000 71.501773 27.054146 12.646702 2.996926 0.001864 0.000000 0.001467 0.410387 5.576975 47.031006
0.350000 70.195627 27.915217 14.060112 3.884495 0.003089 0.000000 0.000875 0.239532 4.157677 42.557557
0.400000 68.918490 28.699796 15.427044 4.903967 0.004735 0.000000 0.000509 0.134158 3.017482 38.337453
0.450000 67.669761 29.412399 16.731608 6.036087 0.008659 0.000000 0.000327 0.072099 2.129141 34.359403
0.500000 66.448845 30.057210 17.964353 7.247447 0.015911 0.000000 0.000205 0.037960 1.451668 30.611971
Lookback Option - Risk-free Rate - Base Case
  Call Option Put Option
strike 20.000000 80.000000 100.000000 120.000000 180.000000 20.000000 80.000000 100.000000 120.000000 180.000000
r                    
0.000000 95.924446 35.924446 15.924446 4.076422 0.023151 0.000000 2.064148 14.214183 34.214183 94.214183
0.050000 94.212180 37.138415 18.113826 5.553327 0.046307 0.000000 1.317869 11.597731 30.622320 87.696085
0.100000 92.887049 38.596804 20.500056 7.361569 0.095033 0.000000 0.817203 9.423784 27.520532 81.810777
0.150000 91.916198 40.273719 23.059560 9.516093 0.184350 0.000000 0.492377 7.642549 24.856709 76.499187
0.200000 91.242619 42.118774 25.744159 11.988607 0.341271 0.000000 0.286271 6.205870 22.580485 71.704331
0.250000 90.809143 44.081096 28.505080 14.727649 0.601280 0.000000 0.161081 5.053608 20.629624 67.357671
0.300000 90.572904 46.123811 31.307447 17.675221 1.012546 0.000000 0.086160 4.138518 18.954883 63.403976
0.350000 90.490567 48.209281 34.115519 20.764490 1.616103 0.000000 0.046001 3.409475 17.503236 59.784522
0.400000 90.525989 50.306786 36.900385 23.935166 2.471254 0.000000 0.024148 2.829644 16.236045 56.455248
0.450000 90.644854 52.387165 39.634602 27.134445 3.659359 0.000000 0.012667 2.365974 15.118537 53.376226
0.500000 90.822620 54.430780 42.300167 30.305644 5.193231 0.000000 0.006573 1.993778 14.124391 50.516231

2. Volatility¶

Volatility has a positive relationship to both call and put options. The volatility affects the magnitude of the randomness term in the risk-neutral walk equation, thus creating more variation in the underlying's asset price. \begin{equation*} S_{t+\delta t} = S_t (1 + r \delta t + \color{red}{\boldsymbol{\sigma}} \phi \sqrt{\delta t}) \end{equation*} This intake produces higher expected payoffs, with ATM options impacted more than ITM and OTM options. The driver for this, is the fact that ATM options have the highest amount of extrinsic value - the uncertainty if the option will expire ITM or OTM. Therefore, when the randomness factor is increased there is a higher possibility that the option will expire at a higher average payoff. This is best seen in the surface plots below, as both options have a curve at the strike 100 and volatility 20% point which becomes more straight as the volatility increases. Interestingly, lookback options are more sensitive to the changes in volatility as their payoff equations are subject to the maximum or minimum of the underlying's price. This payoff structure leverages increased volatility well, as it creates a higher possibility that there will be a higher or lower underlying price. Asian options however, have payoff equations that are subject to the averaging of the underlying price which consequently reduces some of the incorporated volatility.

In [ ]:
filters =  ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic"]
CompareAsianOptions.surface_plot("Asian Option - Volatility - Base Case", filters, "vol", "strike")
filters =  ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Option - Volatility - Base Case",'vol', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed"]
CompareLookbackOptions.surface_plot("Lookback Option - Volatility - Base Case", filters, "vol", "strike")
filters =  ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareLookbackOptions.table("Lookback Option - Volatility - Base Case", 'vol', ['strike'], filters))
Asian Option - Volatility - Base Case
  Call Option Put Option
strike 20.000000 80.000000 100.000000 120.000000 180.000000 20.000000 80.000000 100.000000 120.000000 180.000000
vol                    
0.200000 78.489019 21.456574 5.715737 0.488061 0.000000 0.000000 0.041320 3.325072 17.121984 73.707689
0.400000 78.436757 22.671810 10.025228 3.629354 0.101453 0.000000 1.308819 7.686824 20.315539 73.861404
0.600000 78.351975 25.165575 14.302827 7.748781 1.113225 0.000000 3.887365 12.049206 24.519749 74.957958
0.800000 78.239141 28.149876 18.506939 12.093286 3.486271 0.000000 6.984501 16.366152 28.977087 77.443838
1.000000 78.107491 31.293659 22.605007 16.457675 6.821390 0.001050 10.260983 20.596919 33.474176 80.911657
Lookback Option - Volatility - Base Case
  Call Option Put Option
strike 20.000000 80.000000 100.000000 120.000000 180.000000 20.000000 80.000000 100.000000 120.000000 180.000000
vol                    
0.200000 94.212180 37.138415 18.113826 5.553327 0.046307 0.000000 1.317869 11.597731 30.622320 87.696085
0.400000 111.439250 54.365484 35.340896 20.706412 3.874470 0.000121 9.079875 23.823471 42.848059 99.921825
0.600000 130.612736 73.538970 54.514382 39.148525 15.264108 0.036036 18.264444 34.664276 53.688865 110.762630
0.800000 151.818275 94.744509 75.719921 60.004180 32.091807 0.374664 26.959439 44.187327 63.211916 120.285681
1.000000 175.147787 118.074021 99.049433 83.165520 52.768909 1.254851 34.768165 52.486830 71.511418 128.585184

3. Time to Expiry¶

When analysing the changes in time to expiry, it's important to remember the Euler-Maruyama methods accuracy of $O(\delta t ^{1/2})$. This is because the $\delta t$ term is calculated by dividing the time to expiry by the timesteps. This will cause longer dated options to be less accurate than shorter dated options. With the maximum $time\_to\_expiry = 5$ having an accuracy of $\sqrt{5/252} \approx 0.141$.

The time to expiry affects

  1. both the assets drift and volatility in the risk-neutral random walk equation \begin{equation*} S_{t+\delta t} = S_t (1 + r \color{red}{\boldsymbol{\delta t}} + \sigma \phi \sqrt{ \color{red}{\boldsymbol{\delta t}}}) \end{equation*}
  2. and the discounting factor of the expected payoffs in the option price equation. \begin{equation*} V(S,t) \approx e^{r(\color{red}{\textbf{T - t}})} \frac{1}{n} \sum_{i=1}^n Payoff(S_T^i) \end{equation*}

With the lifetime of the option extended, the impact on drift, volatility and discounting factor becomes more prevalent. This broadly creates higher option prices as the drift and volatility terms have a stronger positive affect on higher expected payoffs.

For ITM options however there can be situations where there is a negative relationship. This is due the volatility term being less impactful for ITM options resulting in the drift and discounting factor terms contributing to most of the price changes. As explained in the the volatility analysis, ITM options are less sensitive to volatility as their extrinsic value is low. This also applies in this case, as the volatility term of the risk-neutral random walk equation is scaled higher as the time to expiry increases. Although, the key point here is that it does not scaled at the same rate as the drift and discount factor terms. Thus for this example, this negative relationship is driven by the following factors:

  1. For ITM put options, the price is mainly reduced due to the drift term of the risk-neutral walk being applied for a longer period of time (as explained in the risk-free rate analysis this would be a increasing factor for call options).
  2. For all ITM options, the intrinsic value of the options is also discounted over a longer amount of time.

Note that this is also subject to the options payoff type. Although this does not affect lookback options as much as Asian options because the lookback option payoff equations are more sensitive to volatility.

This is best seen through a comparison between the below two Asian Option tables, the first shows prices for a risk-free rate at 5% (base case) and the second shows them with a risk-free rate at 0%. With the risk-free rate set to zero, the only contributors to change in price is the volatility. This demonstrates that the drift and discounting factor are the main contributors to these ITM options being less valuable as the time to expiry increases.

In [ ]:
filters =  ["s0 == 100", "r == 0.05", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic"]
CompareAsianOptions.surface_plot("Asian Option - Time to Expiry - Base Case", filters, "time_to_expiry", "strike")
filters =  ["s0 == 100", "r == 0.05", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Option - Time to Expiry - Base Case", 'time_to_expiry', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.0", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Option - Time to Expiry - Risk-free Rate 0%", 'time_to_expiry', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.05", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed"]
CompareLookbackOptions.surface_plot("Lookback Option - Time to Expiry - Base Case", filters, "time_to_expiry", "strike")
filters =  ["s0 == 100", "r == 0.05", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareLookbackOptions.table("Lookback Option - Time to Expiry - Base Case", 'time_to_expiry', ['strike'], filters))
Asian Option - Time to Expiry - Base Case
  Call Option Put Option
strike 20.000000 80.000000 100.000000 120.000000 180.000000 20.000000 80.000000 100.000000 120.000000 180.000000
time_to_expiry                    
1.000000 78.489019 21.456574 5.715737 0.488061 0.000000 0.000000 0.041320 3.325072 17.121984 73.707689
2.000000 77.011391 22.941827 8.628474 2.014509 0.007885 0.000000 0.220681 4.004076 15.486860 67.770481
3.000000 75.566482 24.343974 10.994174 3.768279 0.061669 0.000000 0.419971 4.284330 14.272595 62.208463
4.000000 74.153660 25.618043 13.034442 5.536842 0.225261 0.000000 0.588228 4.379243 13.256257 57.068522
5.000000 72.772295 26.765020 14.838337 7.242681 0.535216 0.000000 0.720772 4.370105 12.350464 52.371047
Asian Option - Time to Expiry - Risk-free Rate 0%
  Call Option Put Option
strike 20.000000 80.000000 100.000000 120.000000 180.000000 20.000000 80.000000 100.000000 120.000000 180.000000
time_to_expiry                    
1.000000 79.982099 20.060640 4.559393 0.301332 0.000000 0.000000 0.078541 4.577294 20.319233 80.017901
2.000000 79.964296 20.459736 6.436585 1.187458 0.004170 0.000000 0.495439 6.472289 21.223161 80.039874
3.000000 79.946597 21.013936 7.870449 2.161585 0.023833 0.000000 1.067339 7.923852 22.214988 80.077236
4.000000 79.929006 21.602826 9.074695 3.105416 0.077270 0.000000 1.673820 9.145689 23.176410 80.148264
5.000000 79.911527 22.191957 10.131602 3.998224 0.176321 0.000000 2.280430 10.220075 24.086696 80.264793
Lookback Option - Time to Expiry - Base Case
  Call Option Put Option
strike 20.000000 80.000000 100.000000 120.000000 180.000000 20.000000 80.000000 100.000000 120.000000 180.000000
time_to_expiry                    
1.000000 94.212180 37.138415 18.113826 5.553327 0.046307 0.000000 1.317869 11.597731 30.622320 87.696085
2.000000 99.481806 45.191560 27.094812 13.286580 1.013639 0.000000 3.069144 14.387420 32.484169 86.774414
3.000000 103.230242 51.587764 34.373604 20.396151 3.387704 0.000000 4.323990 15.787333 33.001493 84.643972
4.000000 106.176906 57.053060 40.678445 26.888528 6.752410 0.000000 5.206194 16.509897 32.884512 82.008357
5.000000 108.605498 61.877451 46.301435 32.862687 10.774766 0.000141 5.820044 16.832312 32.408328 79.136375

4. Geometric vs Arithmetic¶

Geometric averages have a compounding affect which makes them smaller than arithmetic averages. This can be seen in the price difference between geometric and arithmetic Asian options. For fixed strike call options the arithmetic averaged options are higher than their geometric counterparts. This is expected since the payoff equation $\max(A-K,0)$ increases if the $A$ term increases. The inverse happens for put options as a higher $A$ term in the payoff equation $\max(K-A,0)$ produces lower payoffs. The opposite happens for floating strike options as the $A$ term is reversed in the payoff formulas. This affect is less observable when the risk-free rate, volatility and time to expiry values are at the base case (with some comparisons within the Euler-Maruyama accuracy) however it does become more observable at higher values. An example is show in the tables below.

In [ ]:
filters =  ["s0 == 100", "r == 0.05", "vol == 0.2", "time_to_expiry == 5", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Fixed Strike Option: Geometric vs Arithmetic - Base Case", 'averaging_type', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.05", "vol == 0.2", "time_to_expiry == 5", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Floating"]
display(CompareAsianOptions.table("Asian Floating Strike Option: Geometric vs Arithmetic - Base Case", 'averaging_type', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.05", "vol == 0.6", "time_to_expiry == 5", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Fixed Strike Option: Geometric vs Arithmetic - Vol 60%", 'averaging_type', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.05", "vol == 0.6", "time_to_expiry == 5", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Floating"]
display(CompareAsianOptions.table("Asian Floating Strike Option: Geometric vs Arithmetic - Vol 60%", 'averaging_type', ['strike'], filters))
Asian Fixed Strike Option: Geometric vs Arithmetic - Base Case
  Call Option Put Option
strike 20.000000 80.000000 100.000000 120.000000 180.000000 20.000000 80.000000 100.000000 120.000000 180.000000
averaging_type                    
AveragingType.Geometric 71.129708 25.262228 13.532204 6.192702 0.296443 0.000000 0.860567 4.706559 12.943073 53.774860
AveragingType.Arithmetic 72.772295 26.765020 14.838337 7.242681 0.535216 0.000000 0.720772 4.370105 12.350464 52.371047
Asian Floating Strike Option: Geometric vs Arithmetic - Base Case
  Call Option Put Option
strike nan nan
averaging_type    
AveragingType.Geometric 17.335326 4.242826
AveragingType.Arithmetic 16.094025 4.644111
Asian Fixed Strike Option: Geometric vs Arithmetic - Vol 60%
  Call Option Put Option
strike 20.000000 80.000000 100.000000 120.000000 180.000000 20.000000 80.000000 100.000000 120.000000 180.000000
averaging_type                    
AveragingType.Geometric 60.132212 27.913260 21.842116 17.202869 8.860955 0.185568 14.694664 24.199536 35.136304 73.522437
AveragingType.Arithmetic 72.285507 36.995185 30.165265 24.915248 15.054311 0.017588 11.455313 20.201409 30.527408 67.394518
Asian Floating Strike Option: Geometric vs Arithmetic - Vol 60%
  Call Option Put Option
strike nan nan
averaging_type    
AveragingType.Geometric 40.072375 15.982880
AveragingType.Arithmetic 33.620926 21.852706

5. Fixed vs Floating¶

To analyse the price differences between fixed and floating strike options, only ATM fixed strike options are compared with their floating strike option equivalent.

Asian Options¶

Similar to the geometric and arithmetic analysis, arithmetic Asian options at the base case values have a minimal difference between floating strikes and fixed strike prices. The affects become slightly more noticeable at higher values with the floating strike type call and put option prices higher than ATM fixed strike options. The main driver for floating legs having an increased price might be due to the payoff equations having the $S_T$ term, because this term is produced by the risk-neutral random walk equation it is subject to drift and volatility. This inherently will cause the average payoffs to be higher. However, it needs to be mentioned that in these cases most of the comparisons are within the Euler-Maruyama accuracy making the results uncertain. These prices should be re-run with a higher number of simulations and increased timesteps to increase the accuracy of the results.

Geometric Asian options, on the other hand have a wider difference between results, with floating strike call options having a higher price than ATM fixed strike call options and floating strike put options having a smaller price than ATM fixed strike put options. These findings appear to be similar to the geometric and arithmetic results. Considering geometric means are lower than arithmetic means and they are less affect by outliers, it would cause the payoff equations to be higher for call options and lower for put options.

Lookback Options¶

ATM lookback fixed strike calls options are more valuable than floating strikes and floating strike put options are more valuable than ATM fixed strikes. This might be caused by the maximum and minimum terms not having a symmetric distance from $S_0$. Under the risk-neutral random walk equation, the drift term contributes to a higher maximum underlying price, while this works in the opposite direction for minimum prices.

In [ ]:
filters =  ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "averaging_type == @AveragingType.Arithmetic", "vol == 0.2", "strike.isna() | strike == 100"]
display(CompareAsianOptions.table("Asian Option Arithmetic - Fixed vs Floating - Base Case", 'strike_type', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "averaging_type == @AveragingType.Arithmetic", "vol == 0.6", "strike.isna() | strike == 100"]
display(CompareAsianOptions.table("Asian Option Arithmetic - Fixed vs Floating - Vol 60%", 'strike_type', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "averaging_type == @AveragingType.Geometric", "vol == 0.2", "strike.isna() | strike == 100"]
display(CompareAsianOptions.table("Asian Option Geometric - Fixed vs Floating - Base Case", 'strike_type', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "averaging_type == @AveragingType.Geometric", "vol == 0.6", "strike.isna() | strike == 100"]
display(CompareAsianOptions.table("Asian Option Geometric - Fixed vs Floating - Vol 60%", 'strike_type', ['strike'], filters))
filters =  ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000",  "vol == 0.2", "strike.isna() | strike == 100"]
display(CompareLookbackOptions.table("Lookback Option - Fixed vs Floating - Base Case", 'strike_type', ['strike'], filters))
Asian Option Arithmetic - Fixed vs Floating - Base Case
  Call Option Put Option
strike nan 100.000000 nan 100.000000
strike_type        
StrikeType.Fixed nan 5.715737 nan 3.325072
StrikeType.Floating 5.766320 nan 3.320301 nan
Asian Option Arithmetic - Fixed vs Floating - Vol 60%
  Call Option Put Option
strike nan 100.000000 nan 100.000000
strike_type        
StrikeType.Fixed nan 14.302827 nan 12.049206
StrikeType.Floating 14.544228 nan 12.087852 nan
Asian Option Geometric - Fixed vs Floating - Base Case
  Call Option Put Option
strike nan 100.000000 nan 100.000000
strike_type        
StrikeType.Fixed nan 5.505044 nan 3.439906
StrikeType.Floating 5.969047 nan 3.197501 nan
Asian Option Geometric - Fixed vs Floating - Vol 60%
  Call Option Put Option
strike nan 100.000000 nan 100.000000
strike_type        
StrikeType.Fixed nan 12.673742 nan 13.237845
StrikeType.Floating 15.977713 nan 10.703613 nan
Lookback Option - Fixed vs Floating - Base Case
  Call Option Put Option
strike nan 100.000000 nan 100.000000
strike_type        
StrikeType.Fixed nan 18.113826 nan 11.597731
StrikeType.Floating 16.434415 nan 13.277142 nan

Conclusion¶

This report applied the Euler-Maruyama scheme to the Geometric Brownian motion SDE and under the risk-neutral measure $\mathbb{Q}$ defined the risk-neutral random walk equation. This equation was then implemented in Python Juypter Notebooks to produce Monte Carlo simulations of option underlying stock prices. These simulations were used to calculate expected prices for Asian and lookback options by discounting the option's expected payoffs back to present day. Analysis was then conducted to provide insights into how different input data affected the option prices. Most interestingly, the changes in option prices greatly depend on the moneyness of the option with generally ATM options being affected more by changes in input data than ITM and OTM options. Lookback options were also found to have higher prices and be more sensitive to changes than compared with Asian options. Additionally, the report briefly looked into price differences between Asian and lookback averaging types and strike types. It is important to consider these different variations as they can affect option prices in different ways.

References¶

  • CQF Module 3 Lecture 4 (2022) - Dr. Riaz Ahmad, Intro to Numerical Methods
  • CQF Module 3 Lecture 5 (2022) - Dr. Riaz Ahmad, Exotic Options
  • CQF Tutorial 4 (2022) - Dr. Riaz Ahmad, Numerical Methods and Further Topics in Monte Carlo
  • CQF Python Labs 6 (2022) - Kannan Sigaravelu, Monte Carlo Simulation
  • Paul Wilmott (2006), Paul Wilmott on Quantiative Finance Volume 1 & 2 Second Edition
  • Peter Jackel (2002), Monte Carlo Methods in Finance
  • Yves Hilpisch (2019), Python for Finance